diff --git a/samples/initialize_server.py b/samples/initialize_server.py
index 848159a..a3e312c 100644
--- a/samples/initialize_server.py
+++ b/samples/initialize_server.py
@@ -11,7 +11,6 @@ import logging
 import tableauserverclient as TSC
 
 
-
 def main():
     parser = argparse.ArgumentParser(description='Initialize a server with content.')
     parser.add_argument('--server', '-s', required=True, help='server address')
@@ -68,7 +67,8 @@ def main():
         ################################################################################
         # Step 4: Create the project we need only if it doesn't exist
         ################################################################################
-        import time; time.sleep(2)  # sad panda...something about eventually consistent model
+        import time
+        time.sleep(2)  # sad panda...something about eventually consistent model
         all_projects = TSC.Pager(server_upload.projects)
         project = next((p for p in all_projects if p.name.lower() == args.project.lower()), None)
 
diff --git a/tableauserverclient/filesys_helpers.py b/tableauserverclient/filesys_helpers.py
new file mode 100644
index 0000000..0cf304b
--- /dev/null
+++ b/tableauserverclient/filesys_helpers.py
@@ -0,0 +1,6 @@
+ALLOWED_SPECIAL = (' ', '.', '_', '-')
+
+
+def to_filename(string_to_sanitize):
+    sanitized = (c for c in string_to_sanitize if c.isalnum() or c in ALLOWED_SPECIAL)
+    return "".join(sanitized)
diff --git a/tableauserverclient/server/endpoint/datasources_endpoint.py b/tableauserverclient/server/endpoint/datasources_endpoint.py
index 6ab275d..5491736 100644
--- a/tableauserverclient/server/endpoint/datasources_endpoint.py
+++ b/tableauserverclient/server/endpoint/datasources_endpoint.py
@@ -2,6 +2,7 @@ from .endpoint import Endpoint, api, parameter_added_in
 from .exceptions import MissingRequiredFieldError
 from .fileuploads_endpoint import Fileuploads
 from .. import RequestFactory, DatasourceItem, PaginationItem, ConnectionItem
+from ...filesys_helpers import to_filename
 import os
 import logging
 import copy
@@ -77,7 +78,7 @@ class Datasources(Endpoint):
 
         with closing(self.get_request(url, parameters={'stream': True})) as server_response:
             _, params = cgi.parse_header(server_response.headers['Content-Disposition'])
-            filename = os.path.basename(params['filename'])
+            filename = to_filename(os.path.basename(params['filename']))
             if filepath is None:
                 filepath = filename
             elif os.path.isdir(filepath):
diff --git a/tableauserverclient/server/endpoint/groups_endpoint.py b/tableauserverclient/server/endpoint/groups_endpoint.py
index a1485a4..243aa54 100644
--- a/tableauserverclient/server/endpoint/groups_endpoint.py
+++ b/tableauserverclient/server/endpoint/groups_endpoint.py
@@ -58,10 +58,10 @@ class Groups(Endpoint):
     def remove_user(self, group_item, user_id):
         self._remove_user(group_item, user_id)
         try:
-            user_set = group_item.users
-            for user in user_set:
+            users = group_item.users
+            for user in users:
                 if user.id == user_id:
-                    user_set.remove(user)
+                    users.remove(user)
                     break
         except UnpopulatedPropertyError:
             # If we aren't populated, do nothing to the user list
@@ -73,9 +73,9 @@ class Groups(Endpoint):
     def add_user(self, group_item, user_id):
         new_user = self._add_user(group_item, user_id)
         try:
-            user_set = group_item.users
-            user_set.add(new_user)
-            group_item._set_users(user_set)
+            users = group_item.users
+            users.append(new_user)
+            group_item._set_users(users)
         except UnpopulatedPropertyError:
             # If we aren't populated, do nothing to the user list
             pass
diff --git a/tableauserverclient/server/endpoint/workbooks_endpoint.py b/tableauserverclient/server/endpoint/workbooks_endpoint.py
index 850df9f..4d72f69 100644
--- a/tableauserverclient/server/endpoint/workbooks_endpoint.py
+++ b/tableauserverclient/server/endpoint/workbooks_endpoint.py
@@ -3,6 +3,7 @@ from .exceptions import MissingRequiredFieldError
 from .fileuploads_endpoint import Fileuploads
 from .. import RequestFactory, WorkbookItem, ConnectionItem, ViewItem, PaginationItem
 from ...models.tag_item import TagItem
+from ...filesys_helpers import to_filename
 import os
 import logging
 import copy
@@ -112,7 +113,7 @@ class Workbooks(Endpoint):
 
         with closing(self.get_request(url, parameters={"stream": True})) as server_response:
             _, params = cgi.parse_header(server_response.headers['Content-Disposition'])
-            filename = os.path.basename(params['filename'])
+            filename = to_filename(os.path.basename(params['filename']))
             if filepath is None:
                 filepath = filename
             elif os.path.isdir(filepath):

Downloads with invalid characters in the workbook name fail
When downloading workbooks with a name like `Option A or B?` it fails with the error `OSError(22, 'Invalid argument')` on windows because question marks aren't allowed in file names. (Note: when downloading through the browser it converts characters like this to safe ones.. i.e. `>` becomes `_`)

Looking at the [code](https://github.com/tableau/server-client-python/blob/8e6c9078c0f5894e034393f7fcbc9f14f10c5838/tableauserverclient/server/endpoint/workbooks_endpoint.py#L91) I don't see an easy way to overwrite the downloaded workbook name.

Two Requests: 
  1) adding an optional string parameter to set the downloaded workbook name.
  2) adding an optional boolean parameter to download the workbook name the way the browser / server handles it.

(One of these options should take precedence over the other as they are mutually exclusive but both options would be useful).
diff --git a/test/test_datasource.py b/test/test_datasource.py
index ebf17cf..a2732db 100644
--- a/test/test_datasource.py
+++ b/test/test_datasource.py
@@ -145,6 +145,17 @@ class DatasourceTests(unittest.TestCase):
             self.assertTrue(os.path.exists(file_path))
         os.remove(file_path)
 
+    def test_download_sanitizes_name(self):
+        filename = "Name,With,Commas.tds"
+        disposition = 'name="tableau_workbook"; filename="{}"'.format(filename)
+        with requests_mock.mock() as m:
+            m.get(self.baseurl + '/1f951daf-4061-451a-9df1-69a8062664f2/content',
+                  headers={'Content-Disposition': disposition})
+            file_path = self.server.datasources.download('1f951daf-4061-451a-9df1-69a8062664f2')
+            self.assertEqual(os.path.basename(file_path), "NameWithCommas.tds")
+            self.assertTrue(os.path.exists(file_path))
+        os.remove(file_path)
+
     def test_download_extract_only(self):
         # Pretend we're 2.5 for 'extract_only'
         self.server.version = "2.5"
diff --git a/test/test_group.py b/test/test_group.py
index 2f7f227..20c4545 100644
--- a/test/test_group.py
+++ b/test/test_group.py
@@ -92,7 +92,7 @@ class GroupTests(unittest.TestCase):
             m.post(self.baseurl + '/e7833b48-c6f7-47b5-a2a7-36e7dd232758/users', text=response_xml)
             single_group = TSC.GroupItem('test')
             single_group._id = 'e7833b48-c6f7-47b5-a2a7-36e7dd232758'
-            single_group._users = set()
+            single_group._users = []
             self.server.groups.add_user(single_group, '5de011f8-5aa9-4d5b-b991-f462c8dd6bb7')
 
         self.assertEqual(1, len(single_group.users))
diff --git a/test/test_workbook.py b/test/test_workbook.py
index d276ece..0c5ecca 100644
--- a/test/test_workbook.py
+++ b/test/test_workbook.py
@@ -170,6 +170,17 @@ class WorkbookTests(unittest.TestCase):
             self.assertTrue(os.path.exists(file_path))
         os.remove(file_path)
 
+    def test_download_sanitizes_name(self):
+        filename = "Name,With,Commas.twbx"
+        disposition = 'name="tableau_workbook"; filename="{}"'.format(filename)
+        with requests_mock.mock() as m:
+            m.get(self.baseurl + '/1f951daf-4061-451a-9df1-69a8062664f2/content',
+                  headers={'Content-Disposition': disposition})
+            file_path = self.server.workbooks.download('1f951daf-4061-451a-9df1-69a8062664f2')
+            self.assertEqual(os.path.basename(file_path), "NameWithCommas.twbx")
+            self.assertTrue(os.path.exists(file_path))
+        os.remove(file_path)
+
     def test_download_extract_only(self):
         # Pretend we're 2.5 for 'extract_only'
         self.server.version = "2.5"

pip install -e .
parse_log_pytest
requirements.txt
pytest requests-mock
3.6
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs==22.2.0
certifi==2021.5.30
charset-normalizer==2.0.12
idna==3.10
importlib-metadata==4.8.3
iniconfig==1.1.1
packaging==21.3
pluggy==1.0.0
py==1.11.0
pyparsing==3.1.4
pytest==7.0.1
requests==2.11.1
requests-mock==1.12.1
-e git+https://github.com/tableau/server-client-python.git@312069666ba7afeea1093506ee3690d5bba23afb#egg=tableauserverclient
tomli==1.2.3
typing_extensions==4.1.1
urllib3==1.26.20
zipp==3.6.0

name: server-client-python
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=21.2.2=py36h06a4308_0
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - attrs==22.2.0
      - charset-normalizer==2.0.12
      - idna==3.10
      - importlib-metadata==4.8.3
      - iniconfig==1.1.1
      - packaging==21.3
      - pluggy==1.0.0
      - py==1.11.0
      - pyparsing==3.1.4
      - pytest==7.0.1
      - requests==2.11.1
      - requests-mock==1.12.1
      - tomli==1.2.3
      - typing-extensions==4.1.1
      - urllib3==1.26.20
      - zipp==3.6.0
prefix: /opt/conda/envs/server-client-python

test/test_datasource.py::DatasourceTests::test_download_sanitizes_name test/test_group.py::GroupTests::test_add_user test/test_workbook.py::WorkbookTests::test_download_sanitizes_name
test/test_datasource.py::DatasourceTests::test_delete test/test_datasource.py::DatasourceTests::test_download test/test_datasource.py::DatasourceTests::test_download_extract_only test/test_datasource.py::DatasourceTests::test_get test/test_datasource.py::DatasourceTests::test_get_before_signin test/test_datasource.py::DatasourceTests::test_get_by_id test/test_datasource.py::DatasourceTests::test_get_empty test/test_datasource.py::DatasourceTests::test_publish test/test_datasource.py::DatasourceTests::test_publish_invalid_file_type test/test_datasource.py::DatasourceTests::test_publish_missing_mode test/test_datasource.py::DatasourceTests::test_publish_missing_path test/test_datasource.py::DatasourceTests::test_update test/test_datasource.py::DatasourceTests::test_update_copy_fields test/test_datasource.py::DatasourceTests::test_update_missing_id test/test_group.py::GroupTests::test_add_user_before_populating test/test_group.py::GroupTests::test_add_user_missing_group_id test/test_group.py::GroupTests::test_add_user_missing_user_id test/test_group.py::GroupTests::test_create_group test/test_group.py::GroupTests::test_delete test/test_group.py::GroupTests::test_get test/test_group.py::GroupTests::test_get_before_signin test/test_group.py::GroupTests::test_populate_users test/test_group.py::GroupTests::test_remove_user test/test_group.py::GroupTests::test_remove_user_before_populating test/test_group.py::GroupTests::test_remove_user_missing_group_id test/test_group.py::GroupTests::test_remove_user_missing_user_id test/test_workbook.py::WorkbookTests::test_delete test/test_workbook.py::WorkbookTests::test_delete_missing_id test/test_workbook.py::WorkbookTests::test_download test/test_workbook.py::WorkbookTests::test_download_extract_only test/test_workbook.py::WorkbookTests::test_download_missing_id test/test_workbook.py::WorkbookTests::test_get test/test_workbook.py::WorkbookTests::test_get_before_signin test/test_workbook.py::WorkbookTests::test_get_by_id test/test_workbook.py::WorkbookTests::test_get_by_id_missing_id test/test_workbook.py::WorkbookTests::test_get_empty test/test_workbook.py::WorkbookTests::test_populate_connections test/test_workbook.py::WorkbookTests::test_populate_connections_missing_id test/test_workbook.py::WorkbookTests::test_populate_preview_image test/test_workbook.py::WorkbookTests::test_populate_preview_image_missing_id test/test_workbook.py::WorkbookTests::test_populate_views test/test_workbook.py::WorkbookTests::test_populate_views_missing_id test/test_workbook.py::WorkbookTests::test_publish test/test_workbook.py::WorkbookTests::test_publish_invalid_file test/test_workbook.py::WorkbookTests::test_publish_invalid_file_type test/test_workbook.py::WorkbookTests::test_update test/test_workbook.py::WorkbookTests::test_update_copy_fields test/test_workbook.py::WorkbookTests::test_update_missing_id test/test_workbook.py::WorkbookTests::test_update_tags
tableauserverclient/server/endpoint/groups_endpoint.py tableauserverclient/filesys_helpers.py samples/initialize_server.py tableauserverclient/server/endpoint/datasources_endpoint.py tableauserverclient/server/endpoint/workbooks_endpoint.py
tableauserverclient/server/endpoint/groups_endpoint.py tableauserverclient/filesys_helpers.py samples/initialize_server.py tableauserverclient/server/endpoint/datasources_endpoint.py tableauserverclient/server/endpoint/workbooks_endpoint.py