I recently had a need to interact with a Google Sheets spreadsheet and it turned out to be pretty easy and worked very well! I wanted to write this post to share what I learned and hopefully it can help some others out there.
In this guide we’ll explore how to create a Google API key then and use PowerShell to create a new spreadsheets and upload a CSV file into Google Sheets.
Acquiring Google Sheets API
First head to the official Google Developers Console. If you have never used the Google API before go ahead and create a new project. I called mine Scripts-jamesachambers.
After creating a project it should take you to the main dashboard. From here we are going to click “ENABLE APIS AND SERVICES” at the top of the page. It looks like this:
This will bring up a page showing all the different Google APIs available. From here we are going to scroll down to the G-Suite section and click on “Google Sheets” and then choose the “Enable” button.
Enable Google Drive API
Now enable the Google Drive API using the exact same steps. This will be needed for some access/permission controls later.
To enable the Google Drive API follow the same steps we did in the “Acquiring Google Sheets API” but choose “Google Drive” from the G-Suite options instead.
Creating a Service Account
There is a lot of security built into Google’s APIs. Some types of them require user consent but can access private user data. The kind of account we want to create to work with Google Sheets using scripts is a service account.
This type of account uses a certificate to validate your identity instead of a username/password. It is able to create new spreadsheets and give permissions for others to view them without requiring user consent since the service account will own the spreadsheet sidestepping the private user data issue. It is also able to update these sheets right from PowerShell!
When you clicked the “Enable” button it should have taken you into the “Google Sheets API Console” but sometimes I’ve noticed the button just turns into “Manage”. Click Manage of you aren’t already in the Google Sheets developer console. You should be at a screen that looks like this:
Choose the “Credentials” tab on the left hand of the screen as shown above and click “Create Credentials” and choose the “Service Account” option.
Choose a name for your account. You can set an optional description here as well if you would like. Click the “Create” button and it will take you to the next screen.
Give your service account the “Project” – “Owner” role. This will give your service account full access to all resources in the project you created. You can create finer grained permissions if desired but the service account is only going to have access to the project you created so it should be safe unless you have other resources in the project.
Press the button again and leave the final screen on the defaults. Your service account is now created!
Create Service Account Access Key/Certificate
Now that we have created our service account we need to create an access key and download the certificate for it.
At the official Google Developers Console dashboard choose the “Service Accounts” tab from the left side of the screen and then click on the new service account you created:
Make a note or copy of your service account’s email address listed here. This will be the $iss credential we’ll be using in PowerShell to authenticate with Google very shortly.
At the bottom of your “Service account details” page will be an option to add keys. Click the “Add Key” button and choose “Create new key” as shown here:
Choose the “P12” format:
Press “Create”:
A download dialog will pop up and the screen above will be displayed. This is your certificate file your scripts will use to access Google Sheets.
Warning: This is the only time Google will give you this key. If you lose it you cannot recover it or redownload it and have to create a new key and update all your scripts that use that key accordingly. Make sure you have this key backed up!
Once you have safely backed up this key we are all done with configuration and it’s time to head to PowerShell!
Install UMN-Google PowerShell Module
We are going to use the UMN-Google PowerShell module to be able to communicate with Google Sheet’s API. You will want to install this module on any machine you will be running scripts. Open PowerShell and type:
Install-Module UMN-Google
This PowerShell module is developed and maintained by the University of Minnesota. You can see more information at the PowerShell Gallery here: The UMN-Google PowerShellGallery Repository
Getting Authorization Token
This block will appear at the top of your scripts. It will use our service account certificate to get an authorization code from Google.
Create a new PowerShell .ps1 file with the editor of your choice. Copy and paste the code block below:
Import-Module UMN-Google # Set security protocol to TLS 1.2 to avoid TLS errors [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 # Google API Authozation $scope = "https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/drive.file" $certPath = "C:/Users/james/Desktop/PowerShell/jamesachambers-1551564735346.p12" $iss = 'scripts@jamesachambers-1551564735346.iam.gserviceaccount.com' $certPswd = 'notasecret' try { $accessToken = Get-GOAuthTokenService -scope $scope -certPath $certPath -certPswd $certPswd -iss $iss } catch { $err = $_.Exception $err | Select-Object -Property * "Response: " $err.Response } $accessToken
Modify the values below to match your certificate file you downloaded in the previous step and your $iss (service account email address) information that we made a note of/copied earlier. The 2 lines you need to change to your own values are listed in bold.
I recommend fully qualifying the path to your certificate file to avoid working path problems (especially when doing background/automation scheduled tasks). Make sure you remember to copy the certificate from your downloads folder to the location where you created your PowerShell .ps1 file and that you change the path to yours.
After you have modified the values run the script to make sure you don’t receive any errors. If everything ran successfully you will see a valid Google API authorization token! It should look like this:
If your screen shows a long string of numbers/letters/symbols and does not give an error then congratulations, you’ve completed your API key setup successfully! If you received an error double check that you copied the certificate and gave the right path and run through the earlier steps quickly and verify everything looks correct.
Create New Google Sheet
Now that you have working authentication it’s time for the fun part! Now we get to start creating/modifying Google Sheet documents. Let’s create a new sheet and display the spreadsheet ID.
Add the following commands to the end of your .ps1 file with the title of your choice:
# Create new spreadsheet $Title = 'Patching Spreadsheet' $SpreadsheetID = (New-GSheetSpreadSheet -accessToken $accessToken -title $Title).spreadsheetId $SpreadsheetID # Create new sheet $Sheet = 'Computers' Add-GSheetSheet -accessToken $accessToken -sheetName $Sheet -spreadSheetID $SpreadsheetID
Now run your .ps1 file again:
The first line is your access token again, but the second line is the new spreadsheet ID! To access this spreadsheet you append the spreadsheet ID on the end of the following base URL:
https://docs.google.com/spreadsheets/d/
So for example, to access the spreadsheet I just created in this example go to https://docs.google.com/spreadsheets/d/1anI7Bdr0ZjkSDwMbGpCb0KZc1SpSC-3JdQiycvoM5CM/
However, when you go to the URL you will see:
This is because by default these sheets are private. We need to set the permissions for other people to be able to access our new sheet!
Setting Google Sheet Permissions
Let’s start by adding individual users. Comment out the lines that create a new spreadsheet and sheet and add the following lines to the end of the file:
$SpreadsheetID = '1anI7Bdr0ZjkSDwMbGpCb0KZc1SpSC-3JdQiycvoM5CM' Set-GFilePermissions -accessToken $accessToken -fileID $SpreadsheetID -role writer -type user -emailAddress 'youremail@yourdomain.com'
Change the $SpreadsheetID to the ID you received in the last step and the email to match your own personal email address. This will add “writer” permissions to your account to the spreadsheet. The bolded lines are things you need to adjust. The final result should look like this:
Import-Module UMN-Google # Set security protocol to TLS 1.2 to avoid TLS errors [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 # Google API Authozation $scope = "https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/drive.file" $certPath = "C:/Users/james/Desktop/PowerShell/yourcertificate.p12" $iss = 'scripts@jamesachambers-1551564735346.iam.gserviceaccount.com' $certPswd = 'notasecret' try { $accessToken = Get-GOAuthTokenService -scope $scope -certPath $certPath -certPswd $certPswd -iss $iss } catch { $err = $_.Exception $err | Select-Object -Property * "Response: " $err.Response } $SpreadsheetID = '1anI7Bdr0ZjkSDwMbGpCb0KZc1SpSC-3JdQiycvoM5CM' Set-GFilePermissions -accessToken $accessToken -fileID $SpreadsheetID -role writer -type user -emailAddress 'youremail@yourdomain.com'
Now run the script.
If you received the following error:
Invoke-RestMethod : The remote server returned an error: (403) Forbidden.
It means you very likely did not enable the “Google Drive” API. Go back to the “Enable Google Drive API” step from earlier and double check that the Google Drive API got enabled!
Now go back to your browser and refresh the “Access Denied” page. You should now see the blank spreadsheet!
Uploading a CSV File to Google Sheets
Now that you’ve set up your Google Sheets API keys and environment we are ready to modify new Google Sheets! If you already have a CSV file to upload you can use that, otherwise let’s create a quick testing CSV. Open Notepad or your favorite editor up to a new document and paste the following text:
Computer,"Operating System",RAM "iMac 27""",OSX,"16 GB" "HP ZBook 15""",Windows 10,"16 GB"
Now save the document as test.csv in the same directory you have our “Setting Google Sheets Permissions” PowerShell script from the last step. Remove or comment out the very last Set-GFilePermissions line of the script.
Google Sheets expects an ArrayList of values to upload to Google Sheets. We are going to load our test CSV and convert it to an ArrayList and submit the data to Google Sheets with the following code:
# Import CSV $import = New-Object System.Collections.ArrayList($null) # Build sheet header as it isn't included automatically $import.Add( @("Computer", "Operating System", "RAM")) | Out-Null # Build ArrayList $inputCsv = Import-Csv "test.csv" $inputCsv | ForEach-Object { $import.Add( @($_.Computer, $_."Operating System", $_.RAM)) | Out-Null } # Upload CSV data to Google Sheets with Set-GSheetData try { Set-GSheetData -accessToken $accessToken -rangeA1 "A1:C$($import.Count)" -sheetName "Computers" -spreadSheetID $SpreadsheetID -values $import -Debug -Verbose } catch { $err = $_.Exception $err | Select-Object -Property * "Response: " $err.Response }
This code loads the CSV into a variable called $inputCsv and then iterates through each object and adds it to the ArrayList we are going to send to Google Sheets. If you wanted to modify the data before sending it you absolutely can.
Once the CSV is imported you will have an object array and the names of the variables will be the name of the header of that column. You have to be careful if it has a quote in it. You can still access them by quoting the variable. For example:
$_."Operating System"
This will allow you to access the variables even if there are spaces in the column’s header. The actual first line of the CSV with the headers in it is consumed by PowerShell when you import it so we also create our own header line for our spreadsheet.
Your final code should look like this:
Import-Module UMN-Google # Set security protocol to TLS 1.2 to avoid TLS errors [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 # Google API Authozation $scope = "https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/drive.file" $certPath = "C:/Users/james/Desktop/PowerShell/yourcertificate.p12" $iss = 'scripts@jamesachambers-1551564735346.iam.gserviceaccount.com' $certPswd = 'notasecret' try { $accessToken = Get-GOAuthTokenService -scope $scope -certPath $certPath -certPswd $certPswd -iss $iss } catch { $err = $_.Exception $err | Select-Object -Property * "Response: " $err.Response } # Spreadsheet ID # of the Google Sheet $SpreadsheetID = '1anI7Bdr0ZjkSDwMbGpCb0KZc1SpSC-3JdQiycvoM5CM' # Import CSV and build ArrayList $import = New-Object System.Collections.ArrayList($null) $inputCsv = Import-Csv "test.csv" $inputCsv | ForEach-Object { $import.Add( @($_.Computer, $_.'Operating System', $_.RAM)) | Out-Null } # Build sheet header as it isn't included automatically $import.Add( @("Computer", "Operating System", "RAM")) | Out-Null # Build ArrayList # Upload CSV data to Google Sheets with Set-GSheetData try { Set-GSheetData -accessToken $accessToken -rangeA1 "A1:C$($import.Count)" -sheetName "Computers" -spreadSheetID $SpreadsheetID -values $import -Debug -Verbose } catch { $err = $_.Exception $err | Select-Object -Property * "Response: " $err.Response }
Now we’re ready to run the code. Execute your script in PowerShell:
If everything was set up you should immediately see the changes in your browser window and it should look like this:
Wrapping Up
That’s it! You’ve now set up your Google API console access and can now modify Google Sheets through the power of PowerShell automation.
Make sure you check out the University of Minnesota’s GitHub repository to see any new changes or announcements related to the PowerShell extension. It is open source!
International Language Fix (Required for computers not set to “English” as the default language)
Once this post started gaining some traction I learned that international users (specifically ones that didn’t have “English” set as their default language) were having trouble with authentication. I suspected this was due to language differences because I’ve encountered it before on some of my other scripts (like my Minecraft Bedrock dedicated server setup script) that gained international traction as well and once we tested for it sure enough that was the issue.
Yoan graciously shared the solution in the comments:
Function Using-Culture ( [System.Globalization.CultureInfo]$culture = (throw "USAGE: Using-Culture -Culture culture -Script {scriptblock}"), [ScriptBlock]$script= (throw "USAGE: Using-Culture -Culture culture -Script {scriptblock}")) { $OldCulture = [System.Threading.Thread]::CurrentThread.CurrentCulture trap { [System.Threading.Thread]::CurrentThread.CurrentCulture = $OldCulture } [System.Threading.Thread]::CurrentThread.CurrentCulture = $culture Invoke-Command $script [System.Threading.Thread]::CurrentThread.CurrentCulture = $OldCulture }
This was found at Microsoft Developer Blogs – Using Culture Culture ScriptBlock and the blog post contains a ton of information about how to use it and what it’s doing!
In a nutshell it lets you wrap your calls to UMN-Google (or your entire script if you wanted) like this:
Using-Culture en-US {$accessToken = Get-GOAuthTokenService -scope $scope -certPath $certPath -certPswd $certPswd -iss $iss}
This is a really easy way to wrap your functions if you are getting errors related to language differences / default headers / etc. and is definitely worth a try if your PC’s default language is not set to English!
Hello, Sorry for my level of English, I try to make it short! Can you guide me on how to create a list with the elements of the first column of the google sheet? And also how to delete a specific line from the google sheet? (for example in a foreach loop on a compare-object)
Hey Chris,
Welcome! So basically I do something similar for the guide which is building a basic spreadsheet and uploading it. Basically the way it works is we are using the Google API and they expect you to give them an ArrayList essentially of the changes you want to make to the sheet.
You can build this however you want. It’s similar to using Export-CSV and other PowerShell types of functions (although those ones support object arrays and Google wants an actual ArrayList). For example if you just wanted to do the first column you would build an ArrayList containing the first column and then just update the range A1 to A300 or however many rows are in that one column. In other words you can absolutely do this. If you wanted to modify my example you would just change it to only A1 to AX instead of A1 to DX if that makes sense since you only want to update a single column.
For deleting a column it’s very easy. You’re definitely going to want to view the code here as you can just Ctrl+F and find the functions you need:
#region Remove-GSheetSheetRowColumn
function Remove-GSheetSheetRowColumn
{
.Synopsis
Remove row(s) or column(s)
.DESCRIPTION
Remove row(s) or column(s)
.PARAMETER accessToken
access token used for authentication. Get from Get-GOAuthTokenUser or Get-GOAuthTokenService
.PARAMETER startIndex
Index of row or column to start deleting
.PARAMETER endIndex
Index of row or column to stop deleting
.PARAMETER dimension
Remove Rows or Columns
.PARAMETER sheetName
Name of sheet in spreadSheet
.PARAMETER spreadSheetID
ID for the target Spreadsheet. This is returned when a new sheet is created or use Get-GSheetSpreadSheetID
.EXAMPLE Remove-GSheetSheetRowColumn -accessToken $accessToken -sheetName "Sheet1" -spreadSheetID $spreadSheetID -dimension ROWS -startIndex 5 -endIndex 10
#>
Basically since we are accessing the Google API you can do anything the Google API supports but that also means you have to do it Google’s way essentially. For column/cell updates that’s the range method shown in this guide (and you can specify ranges as small or as big as you want) and there’s some other API calls in the UMN-Google code I linked to for things like deleting rows/columns that should make that pretty easy. Those are some tips to help get started at least and hopefully it helps!
Hello, sorry for the late answer, I was on vacation! Thank you for your help. Unfortunately I can’t try it since I have a problem at the beginning with authentication. I can’t find out the reason.
Response:
IsMutuallyAuthenticated : False
Cookies : {}
Headers : {Vary, X-XSS-Protection, X-Frame-Options, X-Content-Type-Options...}
SupportsHeaders : True
ContentLength : -1
ContentEncoding :
ContentType : application/json; charset=UTF-8
CharacterSet : UTF-8
Server : scaffolding on HTTPServer2
LastModified : 09/08/2022 12:15:49
StatusCode : BadRequest
StatusDescription : Bad Request
ProtocolVersion : 1.1
ResponseUri : https://www.googleapis.com/oauth2/v4/token
Method : POST
IsFromCache : False
I created a project, enabled Google Drive and Sheet API, created a service account as owner of project and created the P12 key.
Thank you!
Hey Chris,
Welcome back! I think you may need to use the “International Language Fix (Required for computers not set to “English” as the default language)” fix at the very bottom of this article.
My reasoning for this is that your date looks like en-GB to me since in en-US that would be 08/09/2022 and you are getting a “Bad Request” response. I think that even though I said that English being your default language should do it my guess is that you’re actually being hit with the locale issue as well since that is I assume en-GB or one of the other variations of English but not the US locale.
Can you try wrapping your code with Using-Culture as outlined at the bottom of the article? That should take care of any typical US craziness such as date formats, use of imperial units for measurement in the year 2022 and other such shenanigans (I’m exaggerating a little bit here but that is essentially what Using-Culture does, eliminate many inconsistencies between different languages/locales/cultures that can break encoding). I think that may take care of this for you!
Thank you for your quick answer. It did not work at first, since $accesstoken returned empty. I added $global: to $accessToken, so that it can get out of the try scope.
try {
Using-Culture en-US {$global:accessToken = Get-GOAuthTokenService -scope $scope -certPath $certPath -certPswd $certPswd -iss $iss}
} catch {
$err = $_.Exception
$err | Select-Object -Property *
"Response: "
$err.Response
}
Maybe it’s not necessary and I did something wrong from the beginning ?
It seems to work, I managed to create the sheet (did not changing right access yet)
Hey Chris,
That sounds like permissions issues, I think you may be right about it being something from before. You may need to log into the Google Developer Console and look at the API errors on your account. Almost everyone who has had trouble with this has ended up finding some permissions checkbox somewhere that got missed. I would definitely double check and follow the whole guide again and make sure every single box got checked, both Drive and Sheets are turned on and not just sheets, etc. It’s probably a missing permissions issue or perhaps it’s not finding the right certificate here!
Hi, i am sorry for my bad english but i hope that you will understand me.
in the last section “international language fix” you must wrote this:
$accessToken = Using-Culture en-US {Get-GOAuthTokenService -scope $scope -certPath $certPath -certPswd $certPswd -iss $iss}
Hey Samu,
Welcome! That is correct basically. This will normalize the code and formatting for international languages.
This helps prevent things like escape characters and the like from breaking the code. I’m glad you were able to get it going!
Hello Mr. Chambers
Using your example, I’m inserting a URL into A1:A1of a Google Sheet. I am also using Google’s IMPORTHTML to load ESPN NFL roster member data.
Because of the way IMPORT-CSV works, I forced a TYPE Change to PSCustomObject by modifying the CSV File to only contain a single URL.
I then used the -Header ‘nflstats’ and -Delimiter (x) Options with IMPORT-CSV to ingest the file.
-Header (to simplify the file versus putting the header within the CSV file)
-Delimiter hack (since I control the input, I used the delimiter to force IMPORTHTML to ignore the comma’s (,) within the URL) – if not, the URL would break after the comma (ravens”,) point in the URL
I also removed the second Object.Add(@(“header”)) for the Sheet Header (Breaks IMPORTHTML)
Well, to make a long story short, everything works.
However, instead of insehttps://jamesachambers.com/wp-admin/admin.php?page=theseoframework-settingsrting the URL ASIS, it places an apostrophe (‘) in front of the equal (=) sign of the URL which prevents the import from taking place.
A1:A1 Contains:
‘=IMPORTHTML("https://www.espn.com/nfl/team/roster/_/name/bal/baltimore-ravens","table",1)
Once the apostrophe (‘) is removed, everything works as expected.
Question:
Do you have any idea (pre or post) on data insert or data filter to remove the apostrophe (‘) on insert?
CSV File(testImport.csv) Contains: As Shown
=IMPORTHTML(“https://www.espn.com/nfl/team/roster/_/name/bal/baltimore-ravens”,”table”,1)
Sincerely thanks for the AWESOME script and explanation!!!
FINAL SCRIPT:
Import-Module UMN-Google
# Set security protocol to TLS 1.2 to avoid TLS errors
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
# Google API Authozation
$scope = "https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/drive.file"
$certPath = "C:\PathTo\file.p12"
$iss = 'myaccount.com'
$certPswd = 'notasecret'
try {
$accessToken = Get-GOAuthTokenService -scope $scope -certPath $certPath -certPswd $certPswd -iss $iss
} catch {
$err = $_.Exception
$err | Select-Object -Property *
"Response: "
$err.Response
}
# Spreadsheet ID # of the Google Sheet
$SpreadsheetID = '1wZbvTaDD76okxs7GsYuCjDAR4PJEnaJO2ygNnk8RABU'
# Import CSV and build ArrayList
$import = New-Object System.Collections.ArrayList($null)
$inputCsv = Import-Csv "D:\PatheTo\testImport.csv" -Header 'nflstats' -Delimiter X
$inputCsv | ForEach-Object {
# REMOVED Default Object Values
# $import.Add( @($_.Computer, $_.'Operating System', $_.RAM)) | Out-Null
$import.Add( @($_.nflstats)) | Out-Null
}
# Build sheet header as it isn't included automatically
# REMOVED Default Headers
# $import.Add( @("Computer", "Operating System", "RAM")) | Out-Null
# $import.Add( @("nflstats")) | Out-Null
# Build ArrayList
# Upload CSV data to Google Sheets with Set-GSheetData
try {
# REMOVED Default Sheet Location Range
# Set-GSheetData -accessToken $accessToken -rangeA1 "A1:C$($import.Count)" -sheetName "NFLStatsSpreadSheet" -spreadSheetID $SpreadsheetID -values $import -Debug -Verbose
Set-GSheetData -accessToken $accessToken -rangeA1 "A1:A$($import.Count)" -sheetName "NFLStatsSpreadSheet" -spreadSheetID $SpreadsheetID -values $import -Debug -Verbose
} catch {
$err = $_.Exception
$err | Select-Object -Property *
"Response: "
$err.Response
}
Hey Rick,
Great questions, and you even used a few neat tricks when importing the CSV! So basically the best way to answer your question is to jump right into what UMN-Google is actually doing behind the scenes. It’s not much, but the more valuable part is which PowerShell methods they are calling as that will lead us to some solutions. We can view the code here.
Presumably the issue is something in the process with Set-GSheetData:
function Set-GSheetData
{
...
Begin
{
if ($append)
{
$method = 'POST'
$uri = "https://sheets.googleapis.com/v4/spreadsheets/$spreadSheetID/values/$sheetName"+":append?valueInputOption=$valueInputOption"
}
else
{
$method = 'PUT'
$uri = "https://sheets.googleapis.com/v4/spreadsheets/$spreadSheetID/values/$sheetName!$rangeA1"+"?valueInputOption=$valueInputOption"
}
}
Process
{
$json = @{values=$values} | ConvertTo-Json
Invoke-RestMethod -Method $method -Uri $uri -Body $json -ContentType "application/json" -Headers @{"Authorization"="Bearer $accessToken"}
}
...
}
A couple of things here that may be related to your problem. First all that UMN-Google is doing is wrapping your parameters and passing them to Invoke-RestMethod. It is actually possible to do this yourself without UMN-Google at all (although it’s a fantastic wrapper and you’ll appreciate it even more if you try to do it without it but you can). Basically though it’s possible Invoke-RestMethod’s behavior might explain this.
Check out this post here. This isn’t your exact issue but it’s an example of Invoke-RestMethod doing weird escaping things to strings. This person worked around it by using the “AbsoluteUri” method instead of “Uri”.
As you can see in the UMN-Google code I posted above the library is not using the AbsoluteUri method. It wouldn’t surprise me if that exact same fix either took care of this or had some sort of impact on the characters. Even though this wasn’t meant for an exact fix for this I’m certain it must be a very similar / related issue. Hopefully a peek under the hood of how UMN-Google is working will give you some ideas on where to look / how to proceed if this exact fix doesn’t get it.
Notice that this has to be encoded into JSON as well. That’s the second thing that could be causing this or place where things might be going off the rails. It’s likely something is being escaped somewhere probably according to JSON standards (if this doesn’t end up being explained by Invoke-RestMethod itself or the parameters to it). It’s probably supposed to be removed/unescaped at some point along the way but something isn’t at some point.
This could also be related to language and encoding as well although that is very unlikely if you are based in the US with English as the default language (and becomes more likely if you don’t / aren’t but there’s a lot of previous comments here regarding those issues).
I should also mention it’s possible that the ‘ character needs to be escaped (not sure where in the process but somewhere). This is extremely common in a lot of web languages especially. Lots of vulnerabilities work by inserting a ‘ like that (or sometimes ” or \n null characters, lots of unexpected input). The attacker then starts writing their own code after the single quote which because of the unexpected ‘ input the compiler/shell/application will execute the attacker’s code after that point as if it’s legit (remote code execution / RCE).
You may be creating something like this vulnerability inadvertently. The single quote may just need an escape sequence like \ in front of it. If you add a second matching one it might work afterward, or you may be breaking a sequence by having an unescaped single quote in there at all (I didn’t encounter this during my work with Google Sheets but a few others have also had this issue in the past and it’s a difficult one to solve). It definitely sounds like escaping (or missing escaping that is required) is at play here although there may be more to the story than just that (but I’m hoping this leads you to it).
Hopefully that either gets it for you or gives you an idea of where this is coming from / how to proceed!
Sincerely thank you for freely offering a wealth of help, insight and resources. I’ll dig-in an let you know how it goes!
No problem at all, best of luck!
Hey James, Thank you so much for this – it is exactly what i am looking for although i am not quite there yet with what I am trying to do. I need to be able to replace a column in an existing google sheets. Not sure how I specify this exactly, any pointers?
Hey Nathan,
No sweat at all and great question! You pretty much just overwrite it with what you want it to be. All of my examples here pretty much use the range of A1 to how big the spreadsheet is. This basically means everything on the spreadsheet will be replaced with this operation.
You can absolutely specify a limited range or even a specific cell. You would then just pass the arraylist of values as a list containing one value (for the cell you are replacing). From a practical standpoint I always found it easier to just “rebuild” the sheet when it needed to be updated. If you look in the UMN-Google source code linked from the article you can Ctrl+F and search for “Range” and you will see exactly how they’re using this / how to specify it in there for some examples.
Depending on how complex the application is though it may absolutely be worth writing code to do individual cells. I know others have gone this far and much further here in the comments (they may even have some coded posted if you check for this). This shouldn’t require too much modification to test out with at least. You could try just modifying one cell with a very limited range instead of using a range of A1 to XX and replacing the whole sheet if that makes sense.
Hopefully that helps give an idea of where to start!
Hi James, I can’t seem to see any details on how to set single cells. I have a googleSheet that I want to update data for selected cells. I have found the command -cell but can’t work out how to set the value. Any assistance would be most appreciated
Hey Clare,
Great question! So the best way is to do it the same way as a range update like this guide uses. In the guide I use A1 (the top left cell) for the update but you can submit updates for individual cells. If you wanted to update B13 for example you could use B13 as the start of your range.
It’s a little trickier to get the Google API to accept it because your math has to be exactly right for your range. It shouldn’t be too hard for a single cell. If it’s a group of cells in the middle of the sheet that is where I have seen people have the most trouble trying to do individual updates because it’s tricky to get the exact right starting and ending cells and then put the data in the ArrayList the exact right way.
I think with a range of one though this shouldn’t be too hard to calculate and the ArrayList will only contain one value basically. Hopefully that helps!
Hi James,
Forgive me if this is somewhere in the comments but I would like to be able to do this with google sheets that have been “shared” with me from other users. Every time I attempt to set permissions or get data I receive the “Invoke-RestMethod : The remote server returned an error: (404) Not Found.” message. Is there some sort of permissions on the originators end that need to be enabled/disabled? The client does not want to make the sheet “public”, or else I would simply upload the csv file.
Hey Dan,
This is a great question! When I originally wrote this I was working for a government organization that had a Google contract. I was able to share it from my work account to other people within the organization but because of organization-wide restrictions I wasn’t able to share it with my private gmail address as an example. This is one type of permission to look out for as it’s quite common. I’m not sure where you would add an exception/permissions for this organization-wide as that wasn’t my role but I’m sure it’s possible if you checked the docs / did some searches specifically for that information.
If you/they are trying to share it with another account in the organization then it’s much more likely along the lines of a simple / easy to fix permissions issue. What happens when they try to go to the sheet? Do they get the “You need access” screen like I have in my screenshot in the guide?
Are you using this command:
Set-GFilePermissions -accessToken $accessToken -fileID $SpreadsheetID -role writer -type user -emailAddress 'youremail@yourdomain.com'
One important thing to know is that the sheets are actually Google Drive files. That is why you see GFilePermissions mixed in with some of these commands. It’s possible that you gave Google Sheets permission but not Google Drive permission for example. This tripped me up when I originally set it up and it is in the guide but there’s so many steps and it’s an easy one to miss.
I would definitely have them make sure they have both Google Sheets permissions activated as well as the Google Drive permissions on their service account. This is the easiest to check and probably one of the more likely causes.
Finally, if none of those yield any results you should log into the Google API console. It will tell you the errors with a *little* bit more detail. It will have an error “code” attached to it basically. That 404 not found is not really a HTTP not found error which is what the 404 code actually represents. Google is trying to tell you in a fancy way that something wasn’t found using the using standard HTTP/REST error codes but Google’s API console/dashboard will tell you ever-so-slightly more (and a lot more with a paid storage plan to store more detailed information about the errors is my understanding).
The Google API console should have an error summary for the project that gives you a little bit more that may solve this case for you such as telling you what exactly wasn’t found. It would be helpful to know exactly what wasn’t found. The sheet? The user? Something else entirely? With this we can figure out what settings adjustments we need to make to get it to accept our API call.
Hopefully that helps!
Thanks for the quick reply! So I’m pretty sure you might have answered it in that the creator of the sheet does not have a service account which I would assume from your reply they would need one? Forgive me if this was obvious as I am fairly new to this.
Hey Dan,
Don’t sweat it at all! So no they wouldn’t need to have a service account. The service account is pretty much just for your script but the sheets are stored inside that service account’s Google Drive. Basically once you give permissions to that Google user’s email address they will just be able to go to it in the browser like normal with whatever permissions you gave them (view, edit, etc).
The problem in your case is that call to give permissions to the user. I think you’ll probably need to check the Google API console as it will show all the errors including whatever 404 / permissions error is going on. If I understood your original message that is what is returning the 404.
Do you have access to view the console? There’s a good chance it will be immediately obvious what is wrong. If you go there you should see the project and then click “Go to APIs overview”. You should see the “Errors” summary right in the middle of the project. I can definitely help further for sure if you let me know what kind of errors it’s showing in there!
The console error is “drive.permissions.create:100%”, which is the only error I’m seeing. So I’m thinking that the permissions need to be set by the creator of the sheet? I apologize if this is confusing or I’m not understanding correctly. I designed an app for this client to scrape a website and email them some specific data when the site updates and they just asked me to do the same thing for their google sheet, to which I replied “shouldn’t be too hard!” Famous last words haha. By the way, I REALLY appreciate how responsive and helpful you’ve been!
Hey Dan,
Absolutely. The permissions need to be created by the service account. So basically you need to run the script to give the permissions from the service account the way the guide outlines. Nobody else has any right to delegate the sheet at the moment if that makes sense!
Definitely makes sense. I ran the script exactly as you have it and just changed the spreadsheetID, and email and I’m still receiving the 404 error. I have both drive and sheets APIs enabled in my services account as well. I will continue to play with it, luckily they are not dying for this, and I’ll let you know what I come up with! Thank you again for all the help, I really appreciate it!
Hey James,
So we just had to have me create a sheet with the services account, essentially copy and paste all the data from the existing sheet, then I used the permissions script you have outlined here to make the client a “writer” as well, and everything is good! Thank you so much for all the help!!!
Hey Dan,
No problem, I’m very glad you met with success and thanks for leaving this letting me know!
Hello…
When I push data, the data is always appended with an apostrophe .. why?
Eg my data is:
1
When I view in Sheets it is:
‘1
How to fix?
Hey Matt,
That is a strange one. I take it English is your computer’s default language. It seems like something else is going on than that and even that issue hasn’t ever looked quite like this one.
If I were to guess I would guess that your browser has substituted a single quote to a “browser safe” quote. This has come up a couple of times before on this article but it has been a while and it’s probably pretty buried.
The browser safe quotes have a slight slant to the left or right. There’s a single quote version and a double quote version of these “leaning” quotes. That is what I would check for first as many browsers automatically convert these to try to prevent certain exploits / fix certain encoding issues / etc. See here for an example.
U+0022 QUOTATION MARK " neutral (vertical), used as opening or closing quotation mark; preferred characters in English for paired quotation marks are U+201C and U+201D
U+0027 APOSTROPHE ' neutral (vertical) glyph having mixed usage; preferred character for apostrophe is U+2019; preferred characters in English for paired quotation marks are U+2018 and U+2019
U+0060 GRAVE ACCENT `
U+00B4 ACUTE ACCENT ´
U+2018 LEFT SINGLE QUOTATION MARK ‘
U+2019 RIGHT SINGLE QUOTATION MARK ’ this is the preferred character to use for apostrophe
U+201C LEFT DOUBLE QUOTATION MARK “
U+201D RIGHT DOUBLE QUOTATION MARK ”
I think it’s extremely likely that some of your quotes got replaced by these characters by your browser when copy/pasting. Either that or there’s a typo somewhere in the script but I did actually look (but it’s possible I missed it) and nobody has ever reported this before. An easy way to fix it/test for this is to just type a regular ‘ over all the quotes to be sure it’s not some unicode variant (even if it looks the same in whatever editor you’re using). Hopefully that helps!
Hi there
I’ve got exactly the same problem…
And i dont know how i could fix that :-/
But thanks anyway for this script
csv looks like this:
12.03.2022,Samstag,09:12,2%
in google tabs it looks like this
12.03.2022 Samstag '09:12 '2%
the ‘ only appears if you click inside the field… on the normal view it doesnt appear.. very strange
Hey Andrew,
This is a really tricky one! For sure you’ve seen the other threads already it sounds like in this post related to that too. It’s only a minority of people that get that. The details you gave about how it shows up in Google are interesting and gave me a couple ideas for you to check potentially.
My best guess is that it has to be default language related / formatting related. It doesn’t sound like the other language workaround that I’ve discussed here fixes this issue (it may even cause it while fixing everything else, that’s a possibility). It’s so hard for me to say because my computers don’t produce this issue as they are en-us by default and the issue doesn’t seem to occur for me.
Maybe it’s a Google Sheets issue. I found some people fighting with a similar problem here. It may be related to your date format.
The people in here said using a ISO 8601 date got rid of the single quotes for example. It’s definitely something you are doing different than en-us defaults. That is not to say that is wrong or bad, just that I’m very confident saying at this point that it is something different from an en-us computer’s defaults and the date would fit with that too as that looks different than my date formats.
Maybe you can use the USER_ENTERED formatting / field type they are suggesting in this post too. It honestly could be as simple as changing them to those types within Google Sheets then the uploaded data won’t have them but it’s hard to say.
Hopefully that helps!